本系列文章會在筆者的部落格繼續連載!Design System 101 感謝大家的閱讀!
本文同步上傳到筆者的個人部落格,裡面透過 Sandpack 直接編輯程式碼!
在前一篇文章中,我們透過遍歷 React Children 的方式找到每個子組件的型別,並且將其放入對應的 Slot 中,但這樣的方式有一個缺點,就是當組件的層級越深,就需要越多的遍歷,這樣的效能會變得越來越差。
而今天我們將會介紹如何透過 React Context API 來解決這個問題。
首先先建立一個 Context
const ButtonContext = React.createContext({
slots: {
icon: null,
content: null,
},
});
再來就是透過 ButtonContext.Provider
來包住子組件,並且可以透過 slots
來定義每個 Slot 的內容
const ButtonContextProvider = ({ children, slots }) => {
return <ButtonContext.Provider value={slots}>{children}</ButtonContext.Provider>;
};
最後建立一個 useButtonContext
來取得 Context 的值
const useButtonContext = (props, slotName) => {
const context = React.useContext(ButtonContext);
return { ...(props || {}), ...(context?.[slotName] || {}) };
};
最後再建立 Button 與 Icon 組件時,加入 useButtonContext
來取得相對應 Slot 的內容
const ICONS = {
play: '⏯️',
thumbUp: '👍',
};
const Icon = (props: { type: string }) => {
props = useButtonContext(props, 'icon');
return (
<span style={{ marginRight: '4px', display: 'inline-flex' }} {...props}>
{ICONS[props.type]}
</span>
);
};
const Button = (props: { children: React.ReactNode }) => {
props = useButtonContext(props, 'button');
return (
<button {...props}>
<Icon type="play" />
{props.children}
</button>
);
};
而這就是用 React Context API 建立 Slots 這個概念,也是目前 Adobe React-Spectrum Slot 的設計方式,透過 Context API 來定義 Slots 的內容,並且透過 useSlotProps
來取得相對應 Slot 的值。
接著來探讀 Adobe React-Spectrum Slot 是如何建立 Slots
組件,在這之前,我們不仿想一下如何讓上面的例子適用於不同的組件中,這時我們可能需要以下的 API
Name | Description | Params |
---|---|---|
SlotProvider |
建立 Slots 的 Context | slots |
useSlotProps |
使用對應 Slot 的內容 | props , defaultSlot |
mergeProps |
合併所有的 props | args |
在正式進入實作之前,要先思考一下當開發者對同一個 slot 傳入重複的 props
我們要如何處理,例如以下的例子
<SlotProvider
slots={{ icon: { type: 'play' }, className: 'mb-4', onClick={myClickLogic} }}
>
<SlotProvider
slots={{ icon: { type: 'pause' }, className: 'd-flex', onClick={defaultClickLogic} }}
>
<Button />
</SlotProvider>
</SlotProvider>
這時後我們要保留最外層的 type
(因為這是最後一個傳入的值),但是 className
則是要合併,最後 onClick
這種事件行函式則是要連續的呼叫,這時候我們就可以透過 mergeProps
來解決這個問題。
export const chain = (...fns) => {
return (...args) => {
fns.forEach((fn) => typeof fn === 'function' && fn?.(...args));
};
};
export const mergeProps = (...args) => {
const result = { ...args[0] };
for (let i = 1; i < args.length; i++) {
const props = args[i];
for (const key in props) {
const a = result[key];
const b = props[key];
if (
typeof a === 'function' &&
typeof b === 'function' &&
key.startsWith('on') &&
key.charCodeAt(2) <= 90 &&
key.charCodeAt(2) >= 65
) {
result[key] = chain(a, b);
continue;
}
if (key === 'className') {
result[key] = clsx(a, b);
continue;
}
result[key] = b === undefined ? a : b;
}
}
return result;
};
再來我們就可以實作 SlotProvider
了,因為 React Context 只會取最近的一層 Context,如果也要讓其他開發者放入 slot 參數都可以被取到,透過 useContext
先取得 parentSlots,
透過 reduce
將相同的 slot 組合起來, 並將 slots
與 parentSlots
透過 mergeProps
的方式進行合併,最後用 useMemo
將結果進行儲存,避免重複計算。
const SlotContext = React.createContext(null);
const SlotProvider = (props) => {
const parentSlots = useContext(SlotContext) || {};
const { slots = {}, children } = props;
const value = useMemo(() => {
return Object.keys(parentSlots)
.concat(Object.keys(slots))
.reduce(
(acc, props) => ({
...acc,
[props]: mergeProps(parentSlots[props] || {}, slots[props] || {}),
}),
{},
);
}, [parentSlots, slots]);
return <SlotContext.Provider value={value}>{children}</SlotContext.Provider>;
};
useSlotProps
則相對簡單,我們只需要取得 slot
的值,並且回傳對應的 props
即可。
const useSlotProps = (props, defaultSlot) => {
const slot = props.slot || defaultSlot;
const context = useContext(SlotContext) || {};
return mergeProps(props, mergeProps(slot ? context[slot] : {}, { id: props.id }));
};
最後再將上面的 API 應用到 Button 組件中,首先我們先定義 Button, Icon 組件
const ICONS = {
play: '⏯️',
thumbUp: '👍',
};
const Icon = (props) => {
props = useSlotProps(props, 'icon');
return (
<span style={{ marginRight: '4px', display: 'inline-flex' }} {...props}>
{ICONS[props.type]}
</span>
);
};
const Button = (props) => {
props = useSlotProps(props, 'button');
return (
<button {...props}>
<Icon type="play" />
{props.children}
</button>
);
};
有了 SlotProvider
之後,就可以改 Button 或是 Icon 的參數
const App = () => {
return (
<SlotProvider
slots={{
button: {
style: { display: 'flex', alignItems: 'center' },
},
icon: {
style: { marginRight: '8px', display: 'inline-flex' },
type: 'thumbUp',
},
}}
>
<Button>Play!!</Button>
</SlotProvider>
);
};
明天將介紹 controllState 與 unControllState!